﻿#region Using Statements
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using DirectZZT.Shared.Core;
using DirectZZT.Shared.Scripting;
using DirectZZT.Shared.World;
using DirectZZT.Shared.World.Objects;
#endregion

namespace DirectZZTWorldSerializer
{
    /// <summary>
    /// Provides the default world serializer for DirectZZT worlds.
    /// </summary>
    [Serializable()]
    [Export("DirectZZTWorldSerializer", Type = typeof(IWorldSerializer))]
    public class DirectZZTWorldSerializer : IWorldSerializer
    {
        #region Delegates

        /// <summary>
        /// Delegate used for deserializing worlds, one per version.
        /// </summary>
        /// <param name="document">The document that contains the ZML data.</param>
        /// <param name="destination">The world to deserialize into.</param>
        /// <param name="options">Optional deserialization options.</param>
        /// <returns>Success of the operation.</returns>
        private delegate bool DeserializeWorld(XDocument document, World destination, WorldDeserializeOptions options);

        #endregion

        #region Constants

        private const string SOURCE = "DirectZZTWorldSerializer";
        /// <summary>
        /// Contains the array that defines the supported versions.
        /// </summary>
        private static readonly int[] SUPPORTED_VERSIONS = new int[] { 1 };
        /// <summary>
        /// Defines the current version of the save format.
        /// </summary>
        private static readonly int CURRENT_VERSION = 1;

        #endregion

        #region Fields

        private Dictionary<string, string> _mapping;
        private Dictionary<int, DeserializeWorld> _worldDeserializers;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the DirectZZTWorldSerializer class.
        /// </summary>
        public DirectZZTWorldSerializer()
        {
            _mapping = new Dictionary<string, string>();

            // fetch all concrete types now
            ExportedType[] exports = TypeManager.Instance.GetExports();
            foreach (var item in exports)
            {
                BoardObjectAttribute attribute = BoardObjectAttribute.GetAttribute(item.Type);
                if (attribute != null)
                {
                    _mapping[attribute.Identifier] = item.Type.AssemblyQualifiedName;
                }
            }

            // register deserializers
            _worldDeserializers = new Dictionary<int, DeserializeWorld>();
            _worldDeserializers.Add(1, DeserializeWorldVersion1);
        }

        #endregion

        #region Load world

        /// <summary>
        /// Checks whether or not the world format is supported.
        /// </summary>
        /// <param name="document"></param>
        /// <param name="version"></param>
        /// <returns></returns>
        private bool IsSupported(XDocument document, out int version)
        {
            version = 0;
            if (document == null)
            {
                return false;
            }

            version = document.Root.TryGetAttributeValue("Version", 0);
            if (version == 0)
            {
                return false;
            }

            return SUPPORTED_VERSIONS.Contains(version);
        }

        string IWorldSerializer.Extension
        {
            get { return "zml"; }
        }

        string IWorldSerializer.SaveExtension
        {
            get { return "zsv"; }
        }

        bool IWorldSerializer.IsValid(Stream stream)
        {
            try
            {
                int version = 0;
                XDocument doc = XDocument.Load(stream);
                return IsSupported(doc, out version);
            }
            catch (Exception)
            {
                return false;
            }
        }

        void IWorldSerializer.Load(Stream stream, World destination, WorldDeserializeOptions options)
        {
            XDocument document = XDocument.Load(stream);

            // check if this world is even supported
            int version = 0;
            if (!IsSupported(document, out version))
            {
                Logging.GetLogging().Log(LoggingMessageType.Error, SOURCE, "", "Unkown world format or invalid version detected. Loading aborted.");
                return;
            }

            // check if the deserializer is available for this version
            if (!_worldDeserializers.ContainsKey(version))
            {
                // error!
            }
            else
            {
                _worldDeserializers[version](document, destination, options);
            }
        }

        private void LoadWorldData(XElement root, World world)
        {
            XElement worldDataE = root.Element("WorldData");
            world.Data.TimePassed = worldDataE.TryGetAttributeValue("TimePassed", 0);
            foreach (XElement flagE in worldDataE.Elements("Flag"))
            {
                string name = flagE.TryGetAttributeValue("Name", null);
                string value = flagE.TryGetAttributeValue("Value", null);
                if (name == null)
                {
                    // error 
                    continue;
                }

                world.Data.Flags.Add(name, ScriptFlag.From(value));
            }
        }

        private Board LoadBoard(XElement boardE, Board board)
        {
            board.BoardId = boardE.TryGetAttributeValue("Id", -1);
            board.Name = boardE.TryGetAttributeValue("Name", null);
            board.Title = boardE.TryGetAttributeValue("Title", null);

            // check each board object
            foreach (var boardObjectE in boardE.Element("BoardObjects").Elements())
            {
                string[] tags = boardObjectE.TryGetAttributeValue("Tags", "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                string[] modifiers = boardObjectE.TryGetAttributeValue("Modifiers", "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
                string[] parameters = boardObjectE.TryGetAttributeValue("Parameters", "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                string identifier = boardObjectE.TryGetAttributeValue("Id", null);

                // TODO: don't return null if the identifier wasn't found. rather fill it  into a "unknowns" list!
                if (string.IsNullOrWhiteSpace(identifier) || !_mapping.ContainsKey(identifier))
                {
                    return null;
                }
                Type type = Type.GetType(_mapping[identifier]);
                if (type != null)
                {
                    try
                    {
                        BoardObject boardObject = (BoardObject)Activator.CreateInstance(type);
                        boardObject.Name = boardObjectE.TryGetAttributeValue("Name", null);
                        boardObject.X = boardObjectE.TryGetAttributeValue("X", -1);
                        boardObject.Y = boardObjectE.TryGetAttributeValue("Y", -1);
                        boardObject.Parameters = new List<string>(parameters);
                        boardObject.Tags = tags;
                        if (board.SpawnObject(boardObject, modifiers))
                        {
                            //  load scripts
                            LoadBoardObjectScripts(boardObjectE, boardObject);

                            // success, next one
                            continue;
                        }
                        else
                        {
                            Logging.GetLogging("World").LogFormat(LoggingMessageType.Warning, SOURCE, "", "Another object did already exist at {0}, {1}.", boardObject.X, boardObject.Y);
                        }
                    }
                    catch (Exception ex)
                    {
                        Logging.GetLogging("World").LogFormat(LoggingMessageType.Error, SOURCE, "", "Error spawning object. The error message was: {0}", ex.Message);
                    }
                }
                else
                {
                    Logging.GetLogging("World").LogFormat(LoggingMessageType.Error, SOURCE, "", "There was no BoardObject type found with identifier '{0}'.", identifier);
                }
            }

            return board;
        }

        private void LoadBoardObjectScripts(XElement boardObjectE, BoardObject boardObject)
        {
            foreach (XElement scriptE in boardObjectE.Element("Scripts").Elements("Script"))
            {
                string langStr = scriptE.TryGetAttributeValue("Language", "ZZT-oop");
                ScriptLanguage language = ScriptLanguage.ZZToop;
                Enum.TryParse<ScriptLanguage>(langStr, true, out language);

                int currentInstruction = scriptE.TryGetAttributeValue("CurrentInstruction", 0);

                string[] lines = scriptE.Value.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

                IScript script = Script.Parse(lines, language);
                script.CurrentInstruction = currentInstruction;
                boardObject.AttachScript(script);
            }
        }

        #region World deserializers

        private bool DeserializeWorldVersion1(XDocument document, World destination, WorldDeserializeOptions options)
        {
            try
            {

                destination.FeatureSet = (FeatureSet)Enum.Parse(typeof(FeatureSet), document.Root.TryGetAttributeValue("FeatureSet", "DirectZZT"));
                destination.Author = document.Root.TryGetAttributeValue("Author", "");
                destination.Title = document.Root.TryGetAttributeValue("Title", "");

                LoadWorldData(document.Root, destination);

                XElement boards = document.Root.Element("Boards");
                destination.InitialBoard = boards.TryGetAttributeValue("InitialBoard", 0);
                foreach (var boardE in boards.Elements("Board"))
                {
                    Board board = new Board();
                    destination.AddBoard(board);
                    LoadBoard(boardE, board);
                }
            }
            catch (Exception)
            {
                return false;
            }

            return true;
        }

        #endregion

        #endregion

        #region Save world

        void IWorldSerializer.Save(World world, Stream stream, WorldSerializeOptions options)
        {
            XDocument doc = new XDocument();
            // store general information
            doc.Add(new XElement("DirectZZTWorld"));
            doc.Root.Add(new XAttribute("Version", CURRENT_VERSION));
            doc.Root.Add(new XAttribute("FeatureSet", Enum.GetName(typeof(FeatureSet), world.FeatureSet)));
            doc.Root.Add(new XAttribute("Author", world.Author));
            doc.Root.Add(new XAttribute("Title", world.Title));

            // save world data
            SaveWorldData(doc.Root, world.Data);

            // prepare boards
            XElement boardContainerE = new XElement("Boards");
            boardContainerE.Add(new XAttribute("InitialBoard", world.InitialBoard));

            // write boards
            foreach (Board board in world.Boards)
            {
                SaveBoard(boardContainerE, board);
            }
            doc.Root.Add(boardContainerE);

            doc.Save(stream);
        }

        /// <summary>
        /// Saves the world data.
        /// </summary>
        /// <param name="worldDataContainerE"></param>
        /// <param name="data"></param>
        private void SaveWorldData(XElement worldDataContainerE, WorldData data)
        {
            XElement worldDataE = new XElement("WorldData");
            worldDataE.Add(new XAttribute("TimePassed", data.TimePassed));
            //worldDataE.Add(new XAttribute("Torches", data.Player.Torches));
            //worldDataE.Add(new XAttribute("TorchCycles", data.Player.TorchCycles));
            //worldDataE.Add(new XAttribute("ShotsAmount", data.Player.ShotsAmount));
            //worldDataE.Add(new XAttribute("EnergizerCycles", data.Player.EnergizerCycles));

            foreach (var flag in data.Flags)
            {
                XElement flagE = new XElement("Flag");
                flagE.Add(new XAttribute("Name", flag.Key));
                flagE.Add(new XAttribute("Value", flag.Value.ConvertTo()));
                worldDataE.Add(flagE);
            }

            worldDataContainerE.Add(worldDataE);
        }

        /// <summary>
        /// Saves one board object.
        /// </summary>
        /// <param name="boardContainerE">The Board-container.</param>
        /// <param name="board">The Board data.</param>
        private void SaveBoard(XElement boardContainerE, Board board)
        {
            XElement boardE = new XElement("Board");
            boardE.Add(new XAttribute("Id", board.BoardId));
            boardE.Add(new XAttribute("Name", board.Name));
            boardE.Add(new XAttribute("Title", board.Title));
            boardE.Add(new XAttribute("IsDark", board.IsDark));

            XElement boardObjectsE = new XElement("BoardObjects");
            BoardSerializationData boardData = board.GetSerializationData();
            foreach (var boardObject in boardData.Data)
            {
                SaveBoardObject(boardObjectsE, boardObject);
            }
            boardE.Add(boardObjectsE);

            // add the board
            boardContainerE.Add(boardE);
        }

        /// <summary>
        /// Saves one board object.
        /// </summary>
        /// <param name="boardObjectsE">The BoardObjects-container.</param>
        /// <param name="boardObject">The BoardObject data.</param>
        private void SaveBoardObject(XElement boardObjectsE, BoardObjectSerializationData boardObject)
        {
            XElement element = new XElement("BoardObject");
            element.Add(new XAttribute("Id", boardObject.Id));
            element.Add(new XAttribute("Name", boardObject.Object.Name));
            element.Add(new XAttribute("X", boardObject.Object.X));
            element.Add(new XAttribute("Y", boardObject.Object.Y));
            if (boardObject.Object.Tags != null && boardObject.Object.Tags.Length > 0)
            {
                element.Add(new XAttribute("Tags", string.Join(";", boardObject.Object.Tags)));
            }
            if (boardObject.Object.Parameters != null && boardObject.Object.Parameters.Count > 0)
            {
                element.Add(new XAttribute("Parameters", string.Join(";", boardObject.Object.Parameters)));
            }
            // serialize scripts
            XElement scriptContainer = new XElement("Scripts");
            foreach (var script in boardObject.Object.GetScripts())
            {
                SaveBoardObjectScript(scriptContainer, script);
            }
            element.Add(scriptContainer);
            // TODO: serialize other data
            //foreach (var dataItem in boardObjectData.Object.Data)
            //{
            //    XElement dataItemE = new XElement("Data");
            //    dataItemE.Add(new XAttribute("Id", dataItem.Key));
            //    dataItemE.Add(new XAttribute("Value", dataItem.Value));
            //    element.Add(dataItemE);
            //}

            boardObjectsE.Add(element);
        }

        private void SaveBoardObjectScript(XElement scriptContainer, IScript script)
        {
            XElement element = new XElement("Script");
            element.Add(new XAttribute("Language", Enum.GetName(typeof(ScriptLanguage), script.Language)));
            element.Add(new XAttribute("CurrentInstruction", script.CurrentInstruction));

            string scriptText = string.Join("\n", script.Script);

            element.Add(new XCData(scriptText));
            scriptContainer.Add(element);
        }

        #endregion
    }
}
